<?php
/**
 * In this File the class '\UF\IO\Folder' is defined.
 *
 * @category   UniKap-Framework
 * @package    IO
 * @subpackage Core
 * @author     Ulf -UniKado- Kadner <ulfikado@gmail.com>
 * @since      2014-03-29 16:17
 * @version    0.1
 */

namespace UF\IO
{

    /**
     * A class with some static methods for better folder handlings.
     *
     * @since  v0.1
     */
    class Folder
    {

        # <editor-fold defaultstate="collapsed" desc="- - - >   H I D D E N   C O N S T R U C T O R   - - - - - - - - - - - - - - - - -">

        /**
         * @since  v0.1
         */
        private function __construct() { }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="- - - -   P U B L I C   S T A T I C   M E T H O D S   - - - - - - - - - - - - - -">

        /**
         * Walks up the folder structure $uplevel levels.
         *
         * e.g.: <code>\UF\IO\Folder::Up( '/is/var/www', 2, '' );</code> results in: <code>'/is'</code>
         *
         * @param  string $folder
         * @param  int    $uplevel How many level you need to go up? (Defaults to 1)
         * @param  string $endchar optional end char, append to result (Defaults to a empty string)
         * @return string
         * @since  v0.1
         */
        public static function Up( $folder, $uplevel = 1, $endchar = '' )
        {
            for (; $uplevel > 0; --$uplevel) { $folder = \dirname( $folder ); }
            return $folder . $endchar;
        }

        /**
         * Checks if the folder exists. If so, $folder will be returned as is.
         * Otherwise it returns the first existing parent folder.
         *
         * @param  string $folder
         * @return string
         * @since  v0.1
         */
        public static function GetFirstExisting( $folder )
        {
            $folder = \rtrim( $folder, '\\/' );
            if ( \UF\str_contains( $folder, '/' ) &&
                 \UF\str_contains( $folder, '\\' ) )
            {
                if ( \DIRECTORY_SEPARATOR == '\\' )
                    $folder = \str_replace( '/', '\\', $folder );
                else
                    $folder = \str_replace( '\\', '/', $folder );
            }
            $level = \count(
                \explode(
                    \DIRECTORY_SEPARATOR, \ltrim($folder,'\\/') ) );
            for ( $i = 0; $i < $level && !\is_dir( $folder ); ++$i )
            {
                $f = \dirname( $folder );
                if ( empty( $f ) || $f == $folder ) return '/';
                $folder = $f;
            }
            \clearstatcache();
            return $folder;
        }

        /**
         * Checks if the defined folder can be created. But attention! If the folder already exists
         * it returns FALSE!
         *
         * @param  string $folder_path The folder to check.
         * @return bool
         * @since  v0.1
         */
        public static function CanCreate( $folder_path )
        {
            if ( \is_dir( $folder_path ) ) { return false; }
            $fip = self::GetFirstExisting( $folder_path );
            return \is_writable( $fip );
        }

        /**
         * Creates a new folder with the defined mode.
         *
         * @param  string $folder
         * @param  int    $mode Mode for new folder (0700) only used for unixode systems
         * @throws \UF\IO\Exception If creation fails
         * @since  v0.1
         */
        public static function Create( $folder, $mode = 0700 )
        {
            if ( \is_dir( $folder ) ) { return; }
            if ( !self::CanCreate( $folder ) ) { throw new \UF\IO\Exception(
                $folder, \UF\_( '_io', 'Creation of folder fails cause basefolder isnt writable!' ) ); }
            $res = false;
            try { if ( \DIRECTORY_SEPARATOR == '\\' ) { $res = \mkdir( $folder, null, true ); }
                  else { $res = \mkdir( $folder, $mode, true ); } }
            catch ( \Exception $ex ) { throw new \UF\IO\Exception( $folder,
                \UF\_( '_io', 'Folder creation fails!' ), null, $ex ); }
            if ( !$res || !\is_dir( $folder ) ) { throw new \UF\IO\Exception(
                $folder, \UF\_( '_io', 'Unknown error while executing folder creation.' ) ); }
            \clearstatcache();
        }

        /**
         * Deletes the defined folder recursive.
         *
         * @param  string $folder
         * @throws \UF\IO\Exception
         * @since  v0.1
         */
        public static function Delete( $folder )
        {
            if ( !\is_dir($folder) ) { return; }
            $folder = \rtrim( $folder, '\\/' ) . \DIRECTORY_SEPARATOR;
            $openDir = \opendir( $folder );
            \readdir( $openDir ); \readdir( $openDir );
            while ( FALSE !== ( $item = \readdir( $openDir ) ) ) {
                $path = $folder . $item;
                if ( !\is_dir( $path ) ) { \UF\IO\File::Delete( $path ); }
                else { self::Delete( $path ); } }
            \closedir( $openDir );
            try { \rmdir( $folder ); }
            catch ( \Exception $ex ) {
                throw new \UF\IO\Exception( $folder,
                    $folder, \UF\_( '_io', 'Could not delete the defined folder.' ), null, $ex ); }
        }

        /**
         * Returns all file paths inside the defined folder.
         *
         * @param  string $folder
         * @param  bool $recursive Include also sub folders? (Defaults to FALSE)
         * @return array
         * @since  v0.1
         */
        public static function ListAllFiles( $folder, $recursive = false )
        {
            $res = array();
            if ( \is_dir( $folder ) ) { return $res; }
            if ( !$recursive ) {
                $d = \dir( $folder ); $d->read(); $d->read();
                while ( false !== ( $entry = $d->read() ) ) {
                    $tmp = \UF\IO\Path::Combine( $folder, $entry );
                    if ( !\is_file( $tmp ) ) { continue; }
                    $res[] = $tmp; }
                $d->close();
                return $res; }
            self::_listRecursive( $res, $folder );
            return $res;
        }

        /**
         * Returns all file paths inside the defined folder, matching with name to defined regular expression.
         *
         * @param  string $folder
         * @param  string $filter Regexp for filtering files
         * @param  bool   $recursive Include also sub folders? (Defaults to FALSE)
         * @return array
         * @since  v0.1
         */
        public static function ListFilteredFiles( $folder, $filter, $recursive=false )
        {
            $res = array();
            if ( !\is_dir( $folder ) ) { return $res; }
            if ( $recursive ) {
                self::_listRecursiveFiltered( $res, $filter, $folder );
                return $res; }
            $d = \dir( $folder ); $d->read(); $d->read();
            while ( false !== ( $entry = $d->read() ) ) {
                if ( $entry == '.' || $entry == '..' ) { continue; }
                $tmp = \UF\IO\Path::Combine( $folder, $entry );
                if ( !\is_file( $tmp ) ) { continue; }
                if ( !\preg_match( $filter, $entry ) ) { continue; }
                $res[] = $tmp; }
            $d->close();
            return $res;
        }

        /**
         * Returns the absolute path of $folder, in relation to $basepath. If $folder is allready a absolute
         * path, $basepath will be ignored.
         *
         * The existence of $folder will not be checked!
         *
         * Examples
         *
         * <code>
         * echo "'" . \UF\IO\Folder::GetRealPath('../xyz', '/abc/def') . "'";
         * # outputs '/abc/xyz'
         * echo "'" . \UF\IO\Folder::GetRealPath('./xyz', '/abc/def') . "'";
         * # outputs '/abc/def/xyz'
         * echo "'" . \UF\IO\Folder::GetRealPath('/xyz', '/abc/def') . "'";
         * # outputs '/xyz'
         * echo "'" . \UF\IO\Folder::GetRealPath('C:/xyz', 'C:/abc/def') . "'";
         * # outputs 'C:/xyz'
         * echo "'" . \UF\IO\Folder::GetRealPath('../../xyz', '/abc/def') . "'";
         * # outputs '/xyz'
         * </code>
         *
         * @param  string $folder
         * @param  string $basepath
         * @return string
         * @since  v0.1
         */
        public static function GetRealPath( $folder, $basepath )
        {
            $flen = \strlen( $folder );
            $basepath = \rtrim( \str_replace( '\\', '/', $basepath ), '/' );
            if ( $flen < 1 ) { return $basepath; }
            $folder = \str_replace( '\\', '/', $folder );
            if ( $folder[0] == '/' ) { return $folder; }
            if ( $flen < 2 )
            {
                if ( $folder[0] == '.' ) { return $basepath; }
                return $basepath . '/' . \trim( $folder, '/' );
            }
            if ( $flen == 2 )
            {
                if ( $folder[1] == ':' ) { return $folder . '/'; }
                return $basepath . '/' . \trim( $folder, '/' );
            }
            if ( $folder[1] == ':' && $folder[2] == '/' ) { return $folder; }
            if ( $folder[0] == '.' && $folder[1] == '.' && $folder[2] == '/' )
            {
                $tmp = \explode( '/', \trim( $folder, '/' ) ); $res = '';
                for ( $i = 0; $i < \count($tmp); ++$i )
                {
                    if ( !empty( $res ) ) { $res .= '/' . $tmp[$i]; continue; }
                    if ( $tmp[$i] != '..' ) { $res = $basepath . '/' . $tmp[$i]; continue; }
                    $basepath = \dirname( $basepath );
                }
                if ( empty( $res ) ) { $res = $basepath; }
                return $res;
            }
            if ( $folder[0] == '.' && $folder[1] == '/' ) { return $basepath . '/' . \trim( $folder, './' ); }
            return $basepath . '/' . \trim( $folder, '/' );
        }

        /**
         * Removes all files and folder inside the defined folder. THe folder self will not be deleted!
         *
         * @param  string $folder
         * @throws \UF\IO\Exception
         * @since  v0.1
         */
        public static function Clear( $folder )
        {
            if ( !\is_dir( $folder ) ) return;
            $folder = \rtrim( $folder, '\\/' ) . \DIRECTORY_SEPARATOR;
            $openDir = null;
            try
            {
                $openDir = \opendir( $folder );
                \readdir( $openDir ); \readdir( $openDir );
                while ( false !== ( $item = \readdir( $openDir ) ) )
                {
                    $path = $folder . $item;
                    if ( !\is_dir( $path ) ) \UF\IO\File::Delete( $path );
                    else self::Delete( $path );
                }
                \closedir( $openDir );
            }
            catch ( \Exception $ex )
            {
                if ( \is_resource( $openDir ) ) \closedir( $openDir );
                throw new \UF\IO\Exception(
                    $folder, \UF\_( '_io', 'Cleaning up this folder fails!' ), $ex );
            }
        }

        /**
         * Moves the contents (files + subfolders) from $sourceFolder, to $targetFolder
         *
         * @param  string $sourceFolder The folder with the contents to move (its a ampty folder after)
         * @param  string $targetFolder The target folder
         * @param  int    $tFolderMode  The mode of the target folder if it must becreated (Defaults to 0700)
         * @param  bool   $clearTarget  Empty the target folder before? (Defaults to FALSE)
         * @throws \UF\IO\Exception
         * @since  v0.1
         */
        public static function MoveContents( $sourceFolder, $targetFolder, $tFolderMode = 0700, $clearTarget = false )
        {
            self::Copy( $sourceFolder, $targetFolder, $tFolderMode, $clearTarget );
            $openDir = \opendir( $sourceFolder );
            \readdir( $openDir ); \readdir( $openDir );
            while ( false !== ( $item = \readdir( $openDir ) ) )
            {
                $path = $sourceFolder . '/' . $item;
                if ( !\is_dir( $path ) ) \UF\IO\File::Delete( $path );
                else self::Delete( $path );
            }
            \closedir( $openDir );
        }

        /**
         * Moves the complete folder $sourceFolder with all contents, to $targetFolder. After this action the source
         * folder is deleted!
         *
         * @param  string $sourceFolder The source folder that will be moved
         * @param  string $targetFolder The target folder
         * @param  int    $tFolderMode  The mode of the target folder if it must becreated (Defaults to 0700)
         * @param  bool   $clearTarget  Empty the target folder before? (Defaults to FALSE)
         * @throws \UF\IO\Exception
         * @since  v0.1
         */
        public static function Move( $sourceFolder, $targetFolder, $tFolderMode = 0700, $clearTarget = false )
        {
            self::Copy( $sourceFolder, $targetFolder, $tFolderMode, $clearTarget );
            self::Delete( $sourceFolder );
        }

        /**
         * Copies the whole $sourceFolder contents to $targetFolder.
         *
         * @param  string $sourceFolder The source folder
         * @param  string $targetFolder The target folder
         * @param  int    $tFolderMode  The mode of the target folder if it must becreated (Defaults to 0700)
         * @param  bool   $clearTarget  Empty the target folder before? (Defaults to FALSE)
         * @throws \UF\IO\Exception
         * @since  v0.1
         */
        public static function Copy( $sourceFolder, $targetFolder, $tFolderMode = 0700, $clearTarget = false )
        {
            $sourceFolder = \rtrim( $sourceFolder, '\\/' );
            $targetFolder = \rtrim( $targetFolder, '\\/' );
            if ( !\is_dir( $sourceFolder ) ) throw new \UF\IO\Exception(
                $sourceFolder,
                \UF\_( '_io', 'Can not copy folder contents to another folder if defined source folder dont exists!') );
            if ( \is_dir( $targetFolder ) ) {
                if ( $clearTarget ) self::Clear( $targetFolder ); }
            else self::Create( $targetFolder, $tFolderMode );
            $openDir = \opendir( $sourceFolder );
            \readdir( $openDir ); \readdir( $openDir );
            while ( FALSE !== ( $item = \readdir( $openDir ) ) )
            {
                $spath = $sourceFolder . '/' . $item;
                $tpath = $targetFolder . '/' . $item;
                if ( !\is_dir( $spath ) ) \UF\IO\File::Copy( $spath, $tpath );
                else self::Copy( $spath, $tpath );
            }
            \closedir( $openDir );
        }

        /**
         * Compresses a whole folder to a ZIP archive file.
         *
         * @param  string $sourceFolder The source folder
         * @param  string $zipFile      The ZIP archive file, to create
         * @param  string $zFolderName  The optional folder name for use inside the ZIP achive file
         * @param  bool   $overwrite    Overwrite a existing ZIP archive file? (Defaults to TRUE)
         * @throws \UF\MissingExtensionException If curret used PHP has no ZIP support.
         * @throws \UF\IO\FileAlreadyExistsException
         * @throws \UF\IO\Exception
         * @since  v0.1
         */
        public static function Zip(
            $sourceFolder, $zipFile, $zFolderName=null, $overwrite=true )
        {
            $oldfile = null;
            $res = null;
            if ( !\class_exists('ZipArchive') ) throw new \UF\MissingExtensionException(
                'Zip', \UF\_1( '_io', '\UF\IO\Folder::%s() fails with no ZIP-Support.', 'Zip') );
            if ( \file_exists( $zipFile ) )
            {
                if ( !$overwrite ) throw new \UF\IO\FileAlreadyExistsException(
                    $zipFile, \UF\_( '_io', 'Overwriting the ZIP-file is not allowed by code.' ) );
                $oldfile = $zipFile . '.old';
                \UF\IO\File::Move( $zipFile, $oldfile );
            }
            $zip = new \ZipArchive();
            if ( \TRUE !== ( $res = $zip->open( $zipFile, \ZipArchive::CREATE ) ) )
            {
                if ( !empty( $oldfile ) ) \UF\IO\File::Move( $oldfile, $zipFile );
                self::___handWriteError( $res, $zipFile );
            }
            if ( empty( $zFolderName ) )
            {
                $tmp = \preg_split( '~[\\\\/]~', \dirname( $zipFile ) );
                $zFolderName = $tmp[\count($tmp) - 1];
            }
            $zip->addEmptyDir( $zFolderName );
            $openDir = \opendir( $sourceFolder );
            \readdir( $openDir ); \readdir( $openDir );
            while ( \FALSE !== ( $item = \readdir( $openDir ) ) )
            {
                $itemPath = $sourceFolder . '/' . $item;
                self::___zipItem( $item, $itemPath, $zip, $zFolderName );
            }
            \closedir( $openDir );
            $zip->close();
        }

        # </editor-fold>

        # <editor-fold defaultstate="collapsed" desc="- - - -   P R I V A T E   S T A T I C   M E T H O D S   - - - - - - - - - - - - -">

        /**
         * @since  v0.1
         */
        private static function ___handWriteError( $res, $zipFile )
        {
            throw new \UF\IO\Exception(
                $zipFile, \UF\_( '_io', 'Zipfile could not be created cause ') .
                \UF\IO\ZipArchive::GetZipArchiveError($res));
        }

        /**
         * @since  v0.1
         */
        private static function ___zipItem( $item, $itemPath, \ZipArchive $zip, $zFolderName )
        {
            if ( \is_dir( $itemPath ) )
            {
                $zFolderName .= '/' . $item;
                $zip->addEmptyDir( $zFolderName );
                $openDir = \opendir( $itemPath );
                while ( \FALSE !== ( $itm = \readdir( $openDir ) ) )
                {
                    if ( \in_array( $itm, self::$ignoreItems ) ) continue;
                    $itmPath = $itemPath . '/' . $itm;
                    self::___zipItem( $itm, $itmPath, $zip, $zFolderName );
                }
            }
            else
            {
                $zip->addFile( $itemPath, $zFolderName . '/' . $item );
            }
        }

        /**
         * @since  v0.1
         */
        private static function _listRecursive( &$res, $folder )
        {
            $d = \dir( $folder );
            $d->read(); $d->read();
            while ( false !== ( $entry = $d->read() ) )
            {
                $tmp = \UF\IO\Path::Combine( $folder, $entry );
                if ( \is_dir( $tmp ) ) self::_listRecursive( $res, $tmp );
                else $res[] = $tmp;
            }
            $d->close();
        }

        /**
         * @since  v0.1
         */
        private static function _listRecursiveFiltered( &$res, $filter, $folder)
        {
            $d = \dir( $folder );
            $d->read(); $d->read();
            while ( false !== ( $entry = $d->read() ) )
            {
                $tmp = \UF\IO\Path::Combine( $folder, $entry );
                if ( \is_dir( $tmp ) )
                    self::_listRecursiveFiltered( $res, $filter, $tmp );
                else
                {
                    if ( !\preg_match( $filter, $entry ) ) continue;
                    $res[] = $tmp;
                }
            }
            $d->close();
        }

        # </editor-fold>

    }

}

